/* Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */
#include <assert.h>
#include <libudev.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <regex.h>
#include <syslog.h>

#include "cras_system_state.h"
#include "cras_types.h"
#include "cras_util.h"
#include "cras_checksum.h"

struct udev_callback_data {
	struct udev_monitor *mon;
	struct udev *udev;
	int fd;
};

static unsigned is_action(const char *desired,
			  const char *actual) __attribute__((nonnull(1)));

/* Matches Alsa sound device entries generated by udev.  For
 * example:
 *
 *   /devices/pci0000:00/0000:00:1b.0/sound/card1/pcmC1D0p
 *
 * We want to be able to extract:
 *
 *   o The card number
 *   o The device number
 *   o If it's 'playback' (p) or 'capture' (c). (It may not be both.)
 *
 * Given the example above, the following matches should occur:
 *
 *
 *   |                        A                           |
 *                                                   BBCCCD
 *   /devices/pci0000:00/0000:00:1b.0/sound/card1/pcmC1D10p
 *
 * A: The whole regex will be matched.
 * B: The card.
 * C: The device.
 * D: 'p' (playback) or 'c' (capture)
 *
 * The order of the offsets in the 'pmatch' buffer does not appear
 * to match with the documentation:
 *
 *     Each rm_so element that is not -1 indicates the start
 *     offset of the next largest substring match within the
 *     string.
 *
 * But are, instead, filled in the same order presented in the
 * string.  To alleviate possible issudes, the 'C' (card) and 'D'
 * (device) identifying characters are included in the result.
 */
static const char pcm_regex_string[] = "^.*pcm(C[0-9]+)(D[0-9]+)([pc])";
static regex_t pcm_regex;

/* Card regex is similar to above, but only has one field -- the card. The
 * format is the same with the exception of the leaf node being of the form:
 *
 *  /devices/...../card0
 *
 * Where 0 is the card number and the only thing we care about in
 * this case.
 */

static const char card_regex_string[] = "^.*/card([0-9]+)";
static regex_t card_regex;

static char const * const  subsystem = "sound";
static const unsigned int MAX_DESC_NAME_LEN = 256;

static unsigned is_action(const char *desired, const char *actual)
{
	return actual != NULL && strcmp(desired, actual) == 0;
}

static unsigned is_action_change(const char *action)
{
	return is_action("change", action);
}

static unsigned is_action_remove(const char *action)
{
	return is_action("remove", action);
}

static unsigned is_internal_bus(const char *bus)
{
	return (bus != NULL &&
		(strcmp(bus, "pci") == 0 ||
		 strcmp(bus, "platform") == 0));
}

static unsigned is_external_bus(const char *bus)
{
	return (bus != NULL && (strcmp(bus, "usb") == 0));
}

static unsigned is_internal_device(struct udev_device *dev)
{
	struct udev_device *parent = udev_device_get_parent(dev);
	while (parent != NULL) {
		const char *name = udev_device_get_subsystem(parent);

		if (name != NULL) {
			if (is_external_bus(name))
				return 0;
			else if (is_internal_bus(name))
				return 1;
		}
		parent = udev_device_get_parent(parent);
	}
	return 0;
}

static unsigned is_card_device(struct udev_device  *dev,
			       unsigned		   *internal,
			       unsigned		   *card_number,
			       const char	   **sysname)
{
	regmatch_t m[2];
	const char *devpath = udev_device_get_devpath(dev);

	if (devpath != NULL &&
	    regexec(&card_regex, devpath, ARRAY_SIZE(m), m, 0) == 0) {
		*sysname       = udev_device_get_sysname(dev);
		*internal      = is_internal_device(dev);
		*card_number   = (unsigned)atoi(&devpath[m[1].rm_so]);
		return 1;
	}

	return 0;
}

static void set_factory_default(unsigned card_number)
{
	static const char alsactl[] = "/usr/sbin/alsactl";
	static const char asound_state[] = "/etc/asound.state";
	char cmd_buf[128];
	struct stat stat_buf;
	int r;

	if (stat(asound_state, &stat_buf) == 0) {
		syslog(LOG_INFO, "%s: init card '%u' to factory default",
		       __FUNCTION__, card_number);
		r = snprintf(cmd_buf, ARRAY_SIZE(cmd_buf),
			     "%s --file %s restore %u",
			     alsactl, asound_state, card_number);
		cmd_buf[ARRAY_SIZE(cmd_buf) - 1] = '\0';
		r = system(cmd_buf);
		if (r != 0)
			syslog(LOG_ERR,
			       "%s: failed to init card '%d' "
			       "to factory default.  Failure: %d.  Command: %s",
			       __FUNCTION__, card_number, r, cmd_buf);
	}
}

static inline void udev_delay_for_alsa()
{
	/* Provide a small delay so that the udev message can
	 * propogate throughout the whole system, and Alsa can set up
	 * the new device.  Without a small delay, an error of the
	 * form:
	 *
	 *    Fail opening control hw:?
	 *
	 * will be produced by cras_alsa_card_create().
	 */
	usleep(125000);		/* 0.125 second */
}

/* Reads the "descriptors" file of the usb device and returns the
 * checksum of the contents. Returns 0 if the file can not be read */
static uint32_t calculate_desc_checksum(struct udev_device *dev)
{
	char path[MAX_DESC_NAME_LEN];
	struct stat stat_buf;
	int fd;
	unsigned char *buf = NULL;
	int buf_size = 0;
	int read_size;
	ssize_t n;
	uint32_t result;

	if (snprintf(path, sizeof(path), "%s/descriptors",
		     udev_device_get_syspath(dev)) >= sizeof(path)) {
		syslog(LOG_ERR, "failed to build path");
		return 0;
	}

	if (stat(path, &stat_buf) < 0) {
		syslog(LOG_ERR, "failed to stat file %s: %s",
		       path, strerror(errno));
		return 0;
	}

	fd = open(path, O_RDONLY);
	if (fd < 0) {
		syslog(LOG_ERR, "failed to open file %s: %s",
		       path, strerror(errno));
		return 0;
	}

	read_size = 0;
	while (read_size < stat_buf.st_size) {
		if (read_size == buf_size) {
			if (buf_size == 0)
				buf_size = 256;
			else
				buf_size *= 2;
			uint8_t *new_buf = realloc(buf, buf_size);
			if (new_buf == NULL) {
				syslog(LOG_ERR,
				       "no memory to read file %s", path);
				goto bail;
			}
			buf = new_buf;
		}
		n = read(fd, buf + read_size, buf_size - read_size);
		if (n == 0)
			break;
		if (n < 0) {
			syslog(LOG_ERR, "failed to read file %s", path);
			goto bail;
		}
		read_size += n;
	}

	close(fd);
	result = crc32_checksum(buf, read_size);
	free(buf);
	return result;
bail:
	close(fd);
	free(buf);
	return 0;
}

static void fill_usb_card_info(struct cras_alsa_card_info *card_info,
			       struct udev_device *dev)
{
	const char *sysattr;
	dev = udev_device_get_parent_with_subsystem_devtype(dev,
							    "usb",
							    "usb_device");
	if (!dev)
		return;

	sysattr = udev_device_get_sysattr_value(dev, "idVendor");
	if (sysattr)
		card_info->usb_vendor_id = strtol(sysattr, NULL, 16);
	sysattr = udev_device_get_sysattr_value(dev, "idProduct");
	if (sysattr)
		card_info->usb_product_id = strtol(sysattr, NULL, 16);

	card_info->usb_desc_checksum = calculate_desc_checksum(dev);

	syslog(LOG_ERR, "USB card: vendor:%04x, product:%04x, checksum:%08x",
		card_info->usb_vendor_id, card_info->usb_product_id,
		card_info->usb_desc_checksum);
}

static void device_add_alsa(struct udev_device *dev,
			    const char *sysname,
			    unsigned card,
			    unsigned internal)
{
	struct cras_alsa_card_info card_info;

	udev_delay_for_alsa();
	card_info.card_index = card;
	if (internal) {
		card_info.card_type = ALSA_CARD_TYPE_INTERNAL;
	} else {
		card_info.card_type = ALSA_CARD_TYPE_USB;
		fill_usb_card_info(&card_info, dev);
	}

	cras_system_add_alsa_card(&card_info);
}

void device_remove_alsa(const char *sysname, unsigned card)
{
	udev_delay_for_alsa();
	cras_system_remove_alsa_card(card);
}

static int udev_sound_initialized(struct udev_device *dev)
{
	/* udev will set SOUND_INITALIZED=1 for the main card node when the
	 * system has already been initialized, i.e. when cras is restarted
	 * on an already running system.
	 */
	const char *s;

	s = udev_device_get_property_value(dev, "SOUND_INITIALIZED");
	if (s)
		return 1;

	return 0;
}

static void change_udev_device_if_alsa_device(struct udev_device *dev)
{
	/* If the device, 'dev' is an alsa device, add it to the set of
	 * devices available for I/O.  Mark it as the active device.
	 */
	unsigned	internal;
	unsigned	card_number;
	const char     *sysname;

	if (is_card_device(dev, &internal, &card_number, &sysname) &&
	    udev_sound_initialized(dev) &&
	    !cras_system_alsa_card_exists(card_number)) {
		if (internal)
			set_factory_default(card_number);
		device_add_alsa(dev, sysname, card_number, internal);
	}
}

static void remove_device_if_card(struct udev_device *dev)
{
	unsigned	internal;
	unsigned	card_number;
	const char     *sysname;

	if (is_card_device(dev, &internal, &card_number, &sysname))
		device_remove_alsa(sysname, card_number);
}

static void enumerate_devices(struct udev_callback_data *data)
{
	struct udev_enumerate  *enumerate = udev_enumerate_new(data->udev);
	struct udev_list_entry *dl;
	struct udev_list_entry *dev_list_entry;

	udev_enumerate_add_match_subsystem(enumerate, subsystem);
	udev_enumerate_scan_devices(enumerate);
	dl = udev_enumerate_get_list_entry(enumerate);

	udev_list_entry_foreach(dev_list_entry, dl) {
		const char *path = udev_list_entry_get_name(dev_list_entry);
		struct udev_device *dev =
			udev_device_new_from_syspath(data->udev, path);

		change_udev_device_if_alsa_device(dev);
		udev_device_unref(dev);
	}
	udev_enumerate_unref(enumerate);
}

static void udev_sound_subsystem_callback(void *arg)
{
	struct udev_callback_data *data = (struct udev_callback_data *)arg;
	struct udev_device *dev;

	dev = udev_monitor_receive_device(data->mon);
	if (dev) {
		const char *action = udev_device_get_action(dev);

		if (is_action_change(action))
			change_udev_device_if_alsa_device(dev);
		else if (is_action_remove(action))
			remove_device_if_card(dev);
		udev_device_unref(dev);
	} else
		syslog(LOG_WARNING,
		       "%s (internal error): "
		       "No device obtained", __FUNCTION__);
}

static void compile_regex(regex_t *regex, const char *str)
{
	int r = regcomp(regex, str, REG_EXTENDED);
	assert(r == 0);
}

static struct udev_callback_data udev_data;
void cras_udev_start_sound_subsystem_monitor()
{
	int r;

	udev_data.udev = udev_new();
	assert(udev_data.udev != NULL);
	udev_data.mon = udev_monitor_new_from_netlink(udev_data.udev, "udev");

	udev_monitor_filter_add_match_subsystem_devtype(udev_data.mon,
							subsystem, NULL);
	udev_monitor_enable_receiving(udev_data.mon);
	udev_data.fd = udev_monitor_get_fd(udev_data.mon);

	r = cras_system_add_select_fd(udev_data.fd,
				      udev_sound_subsystem_callback,
				      &udev_data);
	assert(r == 0);
	compile_regex(&pcm_regex, pcm_regex_string);
	compile_regex(&card_regex, card_regex_string);

	enumerate_devices(&udev_data);
}

void cras_udev_stop_sound_subsystem_monitor()
{
	udev_unref(udev_data.udev);
	regfree(&pcm_regex);
	regfree(&card_regex);
}
